﻿#region 참조 네임스페이스

using Skying.ComponentModel;
using Skying.Net;
using Skying.Reflection.Update.Extension.AdobeAir;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Xml;
using System.Xml.Serialization;

#endregion

namespace Skying.Reflection.Update
{
	/// <summary>
	/// <para>업데이트를 위한 인터페이스, 이벤트 등을 제공한다.</para>
	/// </summary>
	public class UpdateManager
	{
		#region 상수

		/// <summary>
		/// 업데이트 매니저의 고유 아이디 상수를 나타낸다.
		/// </summary>
		public const string _UPDATE_MANAGER_GUID = "14EE6CE9-F279-46D3-8BBA-ACDA045E6B16";

		#endregion

		#region 변수

		/// <summary>
		/// 업데이트가 진행 중인지 여부를 나타낸다.
		/// </summary>
		private bool _updating;

		/// <summary>
		/// 
		/// </summary>
		private int _repeatTime;

		/// <summary>
		/// 업데이트를 실행하기 위한 타이머를 나타낸다.
		/// </summary>
		private Timer _updateTimer;

		/// <summary>
		/// 업데이트 게이트웨이 XML을 다운로드 받기 위한 웹 클라이언트를 나타낸다.
		/// </summary>
		private WebClient _schemaWebClient;

		/// <summary>
		/// 다운로드된 업데이트 게이트웨이 XML 정보를 나타낸다.
		/// </summary>
		private UpdateSchema _updateSchema;

		/// <summary>
		/// 업데이트 전체 게이트웨이의 개수를 나타낸다. 업데이트가 필요한지 여부는 각 게이트웨이별 CheckUri를 통해 별도로 확인해야지 가능하다.
		/// </summary>
		private int _totalGatewayCount;

		/// <summary>
		/// 업데이트 점검이 완료된 게이트웨이의 개수를 나타낸다.
		/// </summary>
		private int _checkedGatewayCount;

		/// <summary>
		/// 게이트웨이별 CheckUri를 통해 업데이트를 필요로 하는 게이트웨이의 개수를 나타낸다.
		/// </summary>
		private IList<UpdateSchemaGateway> _targetGateway;

		/// <summary>
		/// 업데이트 점검을 위한 체크 큐를 나타낸다.
		/// </summary>
		private Queue<UpdateSchemaGateway> _queueCheck;

		/// <summary>
		/// 업데이트 게이트웨이의 아이디에 대한 업데이트 예정 파일 사전 목록을 나타낸다.
		/// </summary>
		private IDictionary<string, IList<Assembly>> _reservedAssemblies;

		#endregion

		#region 프로퍼티

		/// <summary>
		/// 업데이트 매니저의 스토리지 경로를 가져온다.
		/// </summary>
		public static string StoragePath
		{
			get
			{
				return string.Format(@"{0}\{1}", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), _UPDATE_MANAGER_GUID);
			}
		}

		/// <summary>
		/// <see cref="Skying.Reflection.Update.UpdateManager"/> 클래스의 객체가 초기화되었는지 여부를 가져온다.
		/// </summary>
		public bool Initialized
		{
			get;
			private set;
		}

		/// <summary>
		/// 업데이트가 시작되면 실행된다.
		/// </summary>
		public Action<AsyncCompletedEventArgs> UpdateStarted
		{
			get;
			set;
		}

		/// <summary>
		/// 업데이트가 중지되면 실행된다.
		/// </summary>
		public Action<AsyncCompletedEventArgs> UpdateStopped
		{
			get;
			set;
		}

		/// <summary>
		/// 업데이트 다운로드가 시작되면 실행된다.
		/// </summary>
		public Action<AsyncCompletedEventArgs> UpdateDownloadStarted
		{
			get;
			set;
		}

		/// <summary>
		/// 업데이트 다운로드 전체 진행률이 변경되면 실행된다.
		/// </summary>
		public Action<AsyncResultEventArgs<int>> UpdateDownloadProgressChanged
		{
			get;
			set;
		}

		/// <summary>
		/// 다운로드 중인 업데이트의 진행률이 변경되면 실행된다.
		/// </summary>
		public Action<AsyncResultEventArgs<DownloadingUpdateEventArgs>> DownloadingUpdateProgressChanged
		{
			get;
			set;
		}

		/// <summary>
		/// 업데이트 다운로드가 완료되면 실행된다.
		/// </summary>
		public Action<AsyncCompletedEventArgs> UpdateDownloadCompleted
		{
			get;
			set;
		}

		/// <summary>
		/// 업데이트 적용이 시작되면 실행된다.
		/// </summary>
		public Action<AsyncCompletedEventArgs> UpdateApplyStarted
		{
			get;
			set;
		}

		/// <summary>
		/// 업데이트 진행률이 변경되면 실행된다.
		/// </summary>
		public Action<AsyncResultEventArgs<int>> UpdateProgressChanged
		{
			get;
			set;
		}

		/// <summary>
		/// 업데이트가 완료되면 실행된다.
		/// </summary>
		public Action<AsyncCompletedEventArgs> UpdateCompleted
		{
			get;
			set;
		}

		/// <summary>
		/// 오류가 발생한 경우 실행된다.
		/// </summary>
		public Action<AsyncResultEventArgs<UpdateException>> Error
		{
			get;
			set;
		}

		#endregion

		#region 생성자/소멸자

		public UpdateManager()
		{
			this._updating = false;

			try
			{
				// 업데이트되어 파일명이 변경된 파일들을 모두 삭제한다.
				FileApplier.DeleteRenamedFiles();

				ApplicationChecker.ExecuteApplication();

				// 웹 클라이언트를 초기화한다.
				this.InitializeSchemaWebClient();

				this.Initialized = true;
			}
			catch
			{
				this.Initialized = false;
			}
		}

		#endregion

		#region 웹 클라이언트 초기화

		/// <summary>
		/// 업데이트 스키마 웹 클라이언트를 초기화한다.
		/// </summary>
		private void InitializeSchemaWebClient()
		{
			this._schemaWebClient = new WebClient();

			// 업데이트 게이트웨이 스키마를 다운로드한 경우 실행된다.
			this._schemaWebClient.DownloadDataCompleted += (sender, e) =>
			{
				if (e.Error != null) return;

				try
				{
					MemoryStream stream = new MemoryStream(e.Result);

					XmlSerializer serializer = new XmlSerializer(typeof(UpdateSchema));

					this._updateSchema = serializer.Deserialize(stream) as UpdateSchema;

					if (this._updateSchema != null)
					{
						this._totalGatewayCount = this._updateSchema.Gateway.Length;

						if (this._updateSchema.Gateway.Length == 0) return;

						this._queueCheck = new Queue<UpdateSchemaGateway>(this._updateSchema.Gateway);

						this.CheckUpdate();
					}
				}
				catch (Exception error)
				{
					UpdateException updateError = new UpdateException(error, UpdateStatus.FailedToDownloadSchema);

					Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(updateError));

					// 오류가 발생했으므로 업데이트 과정을 멈춘다.
					this.Stop();
				}
			};
		}

		#endregion

		#region 업데이트 시작/종료

		/// <summary>
		/// 업데이트 게이트웨이 XML 다운로드를 요청하여 업데이트를 시작한다.
		/// </summary>
		/// <param name="schemaUri">업데이트 게이트웨이 XML을 다운로드 받기 위한 <see cref="System.Uri"/>를 설정한다.</param>
		/// <param name="repeatTime">
		/// <para>업데이트 간의 시간 간격을 설정한다.</para>
		/// <para>업데이트가 진행 중에 타이머는 동작하지 않는다.</para>
		/// </param>
		/// <returns>
		/// <para><see cref="Skying.Reflection.Update.UpdateManager"/> 클래스의 객체가 초기화되었는지 여부가 반환된다.</para>
		/// <para>이 반환 결과는 <see cref="Skying.Reflection.Update.UpdateManager.Initialized"/> 프로퍼티와 동일하다.</para>
		/// </returns>
		public bool Start(Uri schemaUri, int repeatTime)
		{
			// 초기화된 경우에만 시작한다.
			if (this.Initialized)
			{
				this._repeatTime = repeatTime;

				try
				{
					this._updateTimer = new Timer((asyncResult) =>
					{
						this._updating = true;

						Synchronization.Synchronize(this.UpdateStarted);

						this._totalGatewayCount = 0;
						this._checkedGatewayCount = 0;

						this._targetGateway = new List<UpdateSchemaGateway>();

						this._reservedAssemblies = new Dictionary<string, IList<Assembly>>();

						try
						{
							this._schemaWebClient.DownloadDataAsync(schemaUri);
						}
						catch (Exception error)
						{
							UpdateException updateError = new UpdateException(error, UpdateStatus.FailedToDownloadSchema);

							Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(updateError));

							Synchronization.Synchronize(this.UpdateStopped);
						}
					}, null, this._repeatTime, Timeout.Infinite);
				}
				catch (Exception error)
				{
					UpdateException updateError = new UpdateException(error.Message, error, UpdateStatus.FailedToStartOrStop);

					Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(updateError));
				}
			}

			return this.Initialized;
		}

		/// <summary>
		/// 모든 웹클라이언트에 중지를 요청하고 다운로드를 중지하여 업데이트를 중지한다.
		/// </summary>
		public void Stop()
		{
			try
			{
				this._schemaWebClient.CancelAsync();

				this._updateTimer.Change(Timeout.Infinite, Timeout.Infinite);
			}
			catch (Exception error)
			{
				UpdateException updateError = new UpdateException(error, UpdateStatus.FailedToStartOrStop);

				Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(updateError));
			}
			finally
			{
				this._updating = false;

				Synchronization.Synchronize(this.UpdateStopped);
			}
		}

		#endregion

		#region 업데이트 점검

		/// <summary>
		/// 업데이트 점검 큐로부터 업데이트 게이트웨이를 꺼내 업데이트 필요 여부를 점검한다.
		/// </summary>
		private void CheckUpdate()
		{
			if (this._queueCheck.Count == 0) return;

			UpdateSchemaGateway gateway = this._queueCheck.Dequeue();

			WebClient checkWebClient = new WebClient();

			// 업데이트 체크를 다운로드한 경우 실행된다.
			checkWebClient.DownloadDataCompleted += (sender, e) =>
			{
				// 업데이트 체크 개수는 실제 업데이트와는 관계가 없다. 업데이트 점검을 진행하지 못하더라도 포함되어야 한다.
				this._checkedGatewayCount++;

				if (e.Error != null) return;

				try
				{
					using (MemoryStream stream = new MemoryStream(e.Result))
					{
						bool updateRequired = false;

						IList<Assembly> reservedFiles = new List<Assembly>();

						try
						{
							switch ((UpdateTargetTypes)Enum.Parse(typeof(UpdateTargetTypes), gateway.TargetType))
							{
								// 윈도우 응용 프로그램을 위한 게이트웨이인 경우
								case UpdateTargetTypes.WindowsApplicationManifest:

									updateRequired = ManifestChecker.Check(Assembly.GetEntryAssembly(), stream, out reservedFiles);

									this._reservedAssemblies.Add(gateway.Id, reservedFiles);

									break;

								// Adobe AIR 애플리케이션을 위한 게이트웨이인 경우
								case UpdateTargetTypes.AdobeAirApplication:

									updateRequired = ApplicationChecker.Check(stream);

									break;
							}
						}
						catch (Exception error)
						{
							UpdateException updateError = new UpdateException(error, gateway, UpdateStatus.FailedToCheck);

							Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(updateError));
						}

						// 업데이트를 필요로 하는 경우 업데이트 대상으로 추가한다.
						if (updateRequired)
						{
							this._targetGateway.Add(gateway);
						}

						// 모든 업데이트 게이트웨이의 검사가 끝난 경우
						if (this._totalGatewayCount.Equals(this._checkedGatewayCount))
						{
							if (this._targetGateway.Count > 0)
							{
								UpdateDownloader downloader = new UpdateDownloader(this._targetGateway);

								// 전체 업데이트 다운로드 진행률이 변경된 경우 실행된다.
								downloader.DownloadProgressChanged = (asyncResult) =>
								{
									Synchronization.Synchronize(this.UpdateDownloadProgressChanged, new AsyncResultEventArgs<int>(asyncResult.Result));
								};

								// 다운로드 중인 업데이트의 진행률이 변경된 경우 실행된다.
								downloader.DownloadingUpdateProgressChanged = (asyncResult) =>
								{
									Synchronization.Synchronize(this.DownloadingUpdateProgressChanged, new AsyncResultEventArgs<DownloadingUpdateEventArgs>(asyncResult.Result));
								};

								// 전체 업데이트 다운로드가 완료된 경우 실행된다.
								downloader.DownloadCompleted = (asyncResult) =>
								{
									Synchronization.Synchronize(this.UpdateDownloadCompleted);

									FileApplier applier = new FileApplier(asyncResult.Result, this._reservedAssemblies);

									applier.ApplyStarted = (asyncResult2) =>
									{
										Synchronization.Synchronize(this.UpdateApplyStarted);
									};

									applier.ApplyCompleted = (asyncResult2) =>
									{
										Synchronization.Synchronize(this.UpdateCompleted);
									};

									applier.Error = (asyncResult2) =>
									{
										UpdateException exception = new UpdateException(asyncResult2.Error, UpdateStatus.FailedToApply);

										Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(exception));
									};

									applier.Apply();
								};

								// 업데이트 다운로드 중에 오류가 발생된 경우 실행된다.
								downloader.Error = (asyncResult) =>
								{
									UpdateException exception = new UpdateException(asyncResult.Result, gateway, UpdateStatus.FailedToDownloadUpdate);

									Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(exception));
								};

								downloader.Start();

								// UpdateDownloadStarted을 실행하여 업데이트 다운로드를 알린다.
								Synchronization.Synchronize(this.UpdateDownloadStarted);
							}
						}
					}
				}
				catch (Exception error)
				{
					UpdateException exception = new UpdateException(error, gateway, UpdateStatus.FailedToCheck);

					Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(exception));
				}
				finally
				{
					this.CheckUpdate();
				}
			};

			ThreadPool.QueueUserWorkItem((asyncResult) =>
			{
				try
				{
					checkWebClient.DownloadDataAsync(new Uri(gateway.CheckUri), gateway);
				}
				catch (Exception error)
				{
					UpdateException exception = new UpdateException(error, gateway, UpdateStatus.FailedToDownloadUpdate);

					Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(exception));
				}
			});
		}

		#endregion

		#region 어플리케이션 재시작

		/// <summary>
		/// 어플리케이션을 재시작한다.
		/// </summary>
		public void RestartApplication()
		{
			try
			{
				ProcessStartInfo processStartInfo = new ProcessStartInfo()
				{
					Arguments = string.Format("/C ping 127.0.0.1 -n 5 && \"{0}", Assembly.GetEntryAssembly().Location),
					WindowStyle = ProcessWindowStyle.Hidden,
					CreateNoWindow = true,
					FileName = "cmd.exe",
					UseShellExecute = false,
				};

				Process.Start(processStartInfo);

				Environment.Exit(0);
			}
			catch (Exception error)
			{
				UpdateException exception = new UpdateException(error, UpdateStatus.FailedToRestartApplication);

				Synchronization.Synchronize(this.Error, new AsyncResultEventArgs<UpdateException>(exception));
			}
		}

		#endregion
	}
}